"""
base_request.py


"""
import hashlib
import os
import time
from io import BytesIO

import allure
import requests
from requests import sessions

from common.api_log import log
from common.yaml_tools import YamlTools
from config.api_conf import URL, SIGN_SECRET


class BaseRequest:
    sess = requests.session()

    @classmethod
    def send_request(cls, **kwargs):  # 统一请求入口，异常处理
        try:
            rp = cls.sess.request(**kwargs)
        except Exception as e:
            log.error(e)
        else:
            return rp

    @classmethod
    # 生成签名（直接作为私有方法集成到类中）\
    def generate_sign(cls, parametrize_json):
        # 强制校验请求参数中是否包含必要的字段
        required_fields = ['phoneNum', 'optCode']  # 声明一个列表 required_fields，其中包含生成签名必须的字段名
        for field in required_fields:  # 循环遍历：对 required_fields 中的每个字段（phoneNum 和 optCode）进行检查。
            if field not in parametrize_json['json']:  #存在性检查：判断当前字段 field 是否存在于 parametrize_json['json'] 中
                raise ValueError(f"Missing required field: {field}")  # 抛出异常​​：如果字段不存在，抛出 ValueError 并明确提示缺失的字段名。
        # 生成时间戳
        mytime = str(int(time.time() * 1000))
        # 动态json[timestamp]
        parametrize_json["json"]["timestamp"] = mytime
        # 按约定规则拼接字符串（phoneNum + optCode + timestamp ）
        mysign = f"{parametrize_json['json']['phoneNum']}{parametrize_json['json']['optCode']}{parametrize_json['json']['timestamp']}"
        log.info(f"签名字符串: {mysign}")  # 添加日志
        # 加密
        sign = hashlib.md5(mysign.encode()).hexdigest()
        log.info(f"生成签名: {sign}")
        # 生成签名并添加到请求体
        parametrize_json["json"]["sign"] = sign
        return parametrize_json

    @classmethod
    def generate_files(cls, parametrize_files):
        path = parametrize_files.get("files")
        try:
            with open(path, 'rb') as f:  # rb 以二进制模式读取文件，不修改文件内容
                # 这里使用with open打开文件，并在with块内创建files字典。当with块结束时，文件f会被关闭。然而，parametrize_files[
                # "files"]中的文件对象f已经被关闭，因此在发送请求时，requests尝试读取已关闭的文件对象，导致错误。
                file_content = f.read()  # f.read()负责从磁盘读取数据到内存
                # 读取文件内容到内存中，使用BytesIO对象来保存文件内容，这样即使文件被关闭，内存中的内容仍然可用。
                # 构造所需的files参数
                files = {"file": (os.path.basename(path), BytesIO(file_content))}
                # (os.path.basename(path)从完整文件路径中提取文件名（包含扩展名）
                # 使用BytesIO则将数据封装成一个类似文件的对象，可以像操作文件一样操作内存中的数据。
                parametrize_files["files"] = files

        except Exception as e:
            log.error(e)  # 记录错误日志
            raise  # raise时，它会将当前捕获的异常重新抛出，让上层调用者处理。如果不使用raise，异常会被静默处理，程序可能继续执行，但可能导致后续错误。
        return parametrize_files

    @classmethod
    def execute_api_request(cls, request, parametrize):
        # request 传入yaml文件的 request部分，接口的通用信息
        # parametrize 传入的 测试用例列表
        method = request["method"]
        allure.attach(method, f"接口请求方式{method}", attachment_type=allure.attachment_type.TEXT)
        url = URL + request["url"]
        allure.attach(url, f"接口地址为{url}", attachment_type=allure.attachment_type.TEXT)
        cookies = request["cookies"]
        if cookies is not None:
            allure.attach(cookies, f"cookie信息为{cookies}", attachment_type=allure.attachment_type.TEXT)
        headers = request["headers"]
        if headers is not None:
            allure.attach(headers, f"headers信息为{headers}", attachment_type=allure.attachment_type.TEXT)
        data = parametrize["data"]
        if data is not None:
            allure.attach(str(data), f"表单参数信息为{data}", attachment_type=allure.attachment_type.TEXT)
        files_requests = request.get("is_upload",False)
        if files_requests:
            parametrize = cls.generate_files(parametrize)
            files = parametrize["files"]
        else:
            files = parametrize["files"]
        if files is not None:
            allure.attach(str(files), f"json参数为{files}", attachment_type=allure.attachment_type.TEXT)
        files = parametrize["files"]
        if files is not None:
            allure.attach(str(files), f"文件参数信息为{files}", attachment_type=allure.attachment_type.TEXT)
        params = parametrize["params"]
        if params is not None:
            allure.attach(str(params), f"查询字符串参数为{params}", attachment_type=allure.attachment_type.TEXT)
        # 获取签名标记（优先从用例中读取，默认 False）
        sign_required = request.get("sign-required", False)
        # 处理签名逻辑
        if sign_required:
            parametrize = cls.generate_sign(parametrize)
            json = parametrize["json"]
        else:
            json = parametrize["json"]
        if json is not None:
            allure.attach(str(json), f"json参数为{json}", attachment_type=allure.attachment_type.TEXT)

        rp = cls.send_request(method=method, url=url, cookies=cookies, headers=headers, data=data, json=json,
                              files=files, params=params)
        # 处理下载接口
        if request.get("is_download"):
            save_path = parametrize.get("save_path")
            if not save_path:
                raise ValueError("下载接口需配置 save_path 参数")

            # 确保目录存在
            os.makedirs(os.path.dirname(save_path), exist_ok=True)
            # os.path.dirname(save_path) 的作用 提取 save_path 中的 目录部分
            # os.makedirs 自动递归创建所有缺失的目录层级。
            # exist_ok=True：如果目录已存在，不抛出错误
            # 保存文件
            with open(save_path, 'wb') as f:
                f.write(rp.content)

            # 记录结果（非JSON）
            allure.attach(f"文件已保存至: {save_path}", "实际结果", allure.attachment_type.TEXT)
            return rp
        else:
            print(rp.text, rp.json())
            expect = parametrize["expect"]
            allure.attach(str(expect), f"预期结果为{expect}", attachment_type=allure.attachment_type.TEXT)
            return rp


if __name__ == '__main__':
    login_yaml_data = YamlTools.read_yaml_file('api_login_data.yaml')[0]
    rp = BaseRequest.execute_api_request(request=login_yaml_data["request"],
                                         parametrize=login_yaml_data["parametrize"][0])
